4章 Turbo Frame と Turbo Stream テンプレート | Turbo Rails チュートリアル
Turbo Rails Tutorial - Chapter 4 Turbo Frames and Turbo Stream templates
https://www.hotrails.dev/turbo-rails/turbo-frames-and-turbo-streams を読んだメモ。
(略)
ルール2: Turbo Frame内のリンクをクリックしたとき、ターゲットページに同じidのTurbo Frameがない場合、フレームは消え、Response has no matching <turbo-frame id="name_of_the_frame"> elementというエラーがコンソールに記録される。
Quotes#newページに同じidを持つTurbo Frameがなかったときの奇妙な挙動を思い出してください。それこそが、この2つ目のルールの目的です。現在のQuotes#indexページはこのような感じです。
https://scrapbox.io/files/62ff83455f711e0023ca3194.png
今回始めてTurbo Frameを使用した、Quotes#indexページのスケッチ
Quotes#newページで、次のスケッチにあるように、フォームを囲むTurbo Frameのidを "not_matching" に変更しましょう。
https://scrapbox.io/files/62ff845326b564001d42238c.png
マッチするTurbo Frameが存在しないQuotes#newのスケッチ
Quotes#newページのマークアップをスケッチに合わせて更新してみましょう。
code:erb
<%# app/views/quotes/new.html.erb %>
<main class="container">
<%= link_to sanitize("&larr; 見積の一覧に戻る"), quotes_path %>
<div class="header">
<h1>新規見積</h1>
</div>
<%= turbo_frame_tag "not_matching" do %>
<%= render "form", quote: @quote %>
<% end %>
</main>
Quotes#indexページに移動してページを更新し、「新規見積」ボタンをクリックして、もう一度実験してみましょう。ヘッダのあるTurboFrameが消え、Response has no matching <turbo-frame id="name_of_the_frame"> element エラーが予想通りコンソールに出力されました。
Turbo Frame内のリンクをクリックした際、ターゲットページに同じIDのTurbo Frameが存在しない場合、TurboはソースページからTurbo Frameのコンテンツを削除し、エラーをログに出力します。
ルール3: リンクは、data-turbo-frameデータ属性のおかげで、それが直接ネストされているフレームとは別のフレームをターゲットにすることができます。
このルールは非常に便利ですが、明確に理解するためにはもっとスケッチが必要です。まず、Quotes#indexページの見積もりリストの周りに、"second_frame" というidを持つ別のTurbo Frameを追加してみましょう。
https://scrapbox.io/files/62ff8679a8d8f90023f28889.png
Quotes#newのページでは、2番目のフレームと同じidのフレームにフォームを包み込んでみましょう。
https://scrapbox.io/files/62ff8724a8d8f90023f28e73.png
では、スケッチに合わせてコードを更新してみましょう。Quotes#indexページに2番目のTurbo Frameを追加し、さらにこの2番目のTurbo Frameと同じidのdata-turbo-frame data属性を追加する必要があります。
code:erb
<%# app/views/quotes/index.html.erb %>
<main class="container">
<%= turbo_frame_tag "first_turbo_frame" do %>
<div class="header">
<h1>見積の一覧</h1>
<%= link_to "新規見積",
new_quote_path,
data: { turbo_frame: "second_frame" },
class: "btn btn--primary" %>
</div>
<% end %>
<%= turbo_frame_tag "second_frame" do %>
<%= render @quotes %>
<% end %>
</main>
Quote#newページでは、2番目のフレームと同じ名前のTurbo Frameでフォームを包んでみましょう。
code:erb
<%# app/views/quotes/new.html.erb %>
<main class="container">
<%= link_to sanitize("&larr; 見積の一覧に戻る"), quotes_path %>
<div class="header">
<h1>新規見積</h1>
</div>
<%= turbo_frame_tag "second_frame" do %>
<%= render "form", quote: @quote %>
<% end %>
</main>
では、もう一度実験してみましょう。Quotes#index ページを更新し、"新規見積" ボタンをクリックします。すると、見積書リストが新しい見積書フォームに置き換わるのが見えるはずです。これは、data-turbo-frame属性のおかげで、リンクが2番目のフレームをターゲットにしているためです。
data-turbo-frame data属性のおかげで、リンクは直接ネストされていないターボフレームをターゲットにすることができます。その場合、ソース・ページ上のdata-turbo-frame data属性と同じidのTurbo Frameは、ターゲット・ページ上のdata-turbo-frame data属性と同じidのTurbo Frameに置き換わります。
----------
注記:
ページ全体を表す_topという特別なフレームがあります。これは実際にはTurbo Frameではありませんが、ほとんど同じように動作するため、私たちのメンタルモデルではこの近似値を使用することにします。
例えば、"新規見積" ボタンがページ全体を置き換えるようにしたい場合、data-turbo-frame="_top "を次のように使用することができます。
(略)
----------
お気づきのように、TurboフレームはRuby on Rails開発者のツールボックスへの重要な追加要素です。カスタムJavaScriptを一切書かずに、独立したコンテキストでページをスライスできるようになります。
この3つのルールで、見積エディタを構築するためのTurbo Framesの知識は十分すぎるほど得ましたが、まだ2つのことを学ぶ必要があります。
Turbo FramesをTURBO_STREAMフォーマットと組み合わせて使用する方法
なにかいい習慣のもとで Turbo Frame に名前をつける方法
練習して、システムテストに合格できるようにしましょう! しかし、その前に、Quotes#indexページのマークアップを初期状態に戻しておきましょう。
code:erb
<%# app/views/quotes/index.html.erb %>
<main class="container">
<div class="header">
<h1>見積の一覧</h1>
<%= link_to "新規見積", new_quote_path, class: "btn btn--primary" %>
</div>
<%= render @quotes %>
</main>
ターボフレームを用いた見積の編集
2つのうち簡単なほうである、見積編集機能から始めましょう。
まず、Quotes#indexページで引用の「編集」ボタンをクリックすると、見積を含むカードが編集フォームを含むカードに置き換わるようにする必要があります。ウェブページの一部を入れ替えるというのは、まさにTurbo Framesができる仕事です。でも、Turbo FramesにどんなIDをつければいいのでしょうか?
Quotes#indexページで、各引用符を囲むTurbo Frameには、一意なidを付ける必要があります。モデル名の後に引用のIDを付けるのが良い方法です。どのように見えるかスケッチしてみましょう。
https://scrapbox.io/files/62ffc291c18e9d0021d542f9.png
ここで、2つ目の見積書を編集したいとします。2つ目の見積書の「編集」ボタンをクリックしたとき、次のスケッチのようにQuotes#editページに同じidを持つTurbo Frameが必要になります。
https://scrapbox.io/files/62ffc5cf19c5a7001dabd671.png
次のスケッチのように、ターボフレームに適切な名前をつけて、Quotes#indexページで2番目の見積の「編集」ボタンをクリックすると、2番目の見積を含むターボフレームの中身を、フォームを含むターボフレームの中身で置き換えられるようにします。
https://scrapbox.io/files/62ffc8ee1025cb001d0dbb09.png
これらのスケッチと前節のルールを念頭に置いて、この動作を実装してみましょう。Quotes#indexページでは、各見積を "quote_#{quote_id}" というidを持つTurbo Frameで囲みましょう。Quotes#indexページの各見積もりカードは_quote.html.erbパーシャルからレンダリングされるので、このidで各見積もりをTurbo Frameにラップすればよいのです。
code:erb
<%# app/views/quotes/_quote.html.erb %>
<%= turbo_frame_tag "quote_#{quote.id}" do %>
<div class="quote">
<%= link_to quote.name, quote_path(quote) %>
<div class="quote__actions">
<%= button_to "削除",
quote_path(quote),
method: :delete,
class: "btn btn--light" %>
<%= link_to "編集",
edit_quote_path(quote),
class: "btn btn--light" %>
</div>
</div>
<% end %>
Quotes#editページのフォームも、同じidのTurbo Frameで囲う必要があります。
code:erb
<%# app/views/quotes/edit.html.erb %>
<main class="container">
<%= link_to sanitize("&larr; 見積の一覧に戻る"), quote_path(@quote) %>
<div class="header">
<h1>見積の編集</h1>
</div>
<%= turbo_frame_tag "quote_#{@quote.id}" do %>
<%= render "form", quote: @quote %>
<% end %>
</main>
さて、この4行のコードだけを追加して、ブラウザでコードを試してみましょう。見積の「編集」ボタンをクリックしてみましょう。フォームが正常に見積カードを置き換えています。
フォームを送信して、期待通りに動作するか確認してみましょう。
(略)
Turbo Frameとdom_idヘルパー
turbo_frame_tag ヘルパーについて知っておくべきことがもう一つあります。このヘルパーには、文字列か dom_id に変換可能な任意のオブジェクトを渡すことができます。dom_idヘルパーは、このようにオブジェクトを一意のidに変換するのに役立ちます。
code:ruby
# 見積もりが永続化され、そのIDが1の場合。
dom_id(@quote) # => "quote_1"
# もし見積が新しいレコードである場合。
dom_id(Quote.new) # => "new_quote" # 引用が新しいレコードの場合: dom_id(@quote) # => "new_quote"
# dom_id はオプションでプレフィックスの引数を取ることができることに注意してください。
# これはチュートリアルの後半で使用します。
dom_id(Quote.new, "prefix") # "prefix_new_quote"
turbo_frame_tag ヘルパーは、与えられたオブジェクトを自動的に dom_id に渡します。したがって、Quotes#index と Quotes#edit ビューの2つの turbo_frame_tag 呼び出しは、文字列の代わりにオブジェクトを渡すことでリファクタリングすることができます。以下のコードブロックは等価です。
code:erb
<%= turbo_frame_tag "quote_#{@quote.id}" do %>
...
<% end %>
<%= turbo_frame_tag dom_id(@quote) do %>
...
<% end %>
<%= turbo_frame_tag @quote %>
...
<% end %>
先ほど書いたコードをリファクタリングして、この糖衣構文を使えるようにしてみましょう。
code:erb
<%# app/views/quotes/_quote.html.erb %>
<%= turbo_frame_tag quote do %>
<div class="quote">
<%= link_to quote.name, quote_path(quote) %>
<div class="quote__actions">
<%= button_to "Delete",
quote_path(quote),
method: :delete,
class: "btn btn--light" %>
<%= link_to "Edit",
edit_quote_path(quote),
class: "btn btn--light" %>
</div>
</div>
<% end %>
code:erb
<%# app/views/quotes/edit.html.erb %>
<main class="container">
<%= link_to sanitize("&larr; 見積に戻る"), quote_path(@quote) %>
<div class="header">
<h1>見積の編集</h1>
</div>
<%= turbo_frame_tag @quote do %>
<%= render "form", quote: @quote %>
<% end %>
</main>
Turbo Frameに素敵な名前がついたことで、私たちは仕事を続けることができるようになりました。
見積の表示と削除
上記の実装は、#editと#updateのアクションについてはうまく機能しますが、2つの新しい問題が発生しました。
見積を表示するリンクが期待通りに動作しない。見積を含むTurbo Frameが消え、コンソールにエラーが記録される。
見積書を削除するボタンは、コンソールにエラーを記録します。
これらの問題はいずれも、さきほどのTurbo Frameのルール2に関係しています。ひとつひとつ解決していきましょう。
Quotes#indexのページは、現在このような感じになっています。
https://scrapbox.io/files/6300014e9d8f45001daf5b8c.png
見ての通り、見積を表示するためのリンクは、id "quote_#{quote.id}"のTurbo Frame内にネストされています。しかし、Quotes#showページでは、同じidを持つTurbo Frameは存在しません。
https://scrapbox.io/files/63000209d453350022bec926.png
TurboはQuotes#showページに同じidのTurbo Frameが存在することを期待しています。この問題を解決するために、Quotes#showページへのリンクは "_top" フレームをターゲットにして、ページ全体を入れ替えるようにします。
code:erb
<%# app/views/quotes/_quote.html.erb %>
<%= turbo_frame_tag quote do %>
<div class="quote">
<%= link_to quote.name,
quote_path(quote),
data: { turbo_frame: "_top" } %>
<div class="quote__actions">
<%= button_to "削除",
quote_path(quote),
method: :delete,
class: "btn btn--light" %>
<%= link_to "編集",
edit_quote_path(quote),
class: "btn btn--light" %>
</div>
</div>
<% end %>
ブラウザでテストしてみましょう。最初の問題は解決しました。Quotes#showのページへのリンクが期待通りに機能するようになりました。
引用を削除するフォームのターゲットを"_top" フレームにすることで、同じ方法で2つ目の問題を解決することができます。
code:erb
<%# app/views/quotes/_quote.html.erb %>
<%= turbo_frame_tag quote do %>
<div class="quote">
<%= link_to quote.name,
quote_path(quote),
data: { turbo_frame: "_top" } %>
<div class="quote__actions">
<%= button_to "削除",
quote_path(quote),
method: :delete,
form: { data: { turbo_frame: "_top" } },
class: "btn btn--light" %>
<%= link_to "編集",
edit_quote_path(quote),
class: "btn btn--light" %>
</div>
</div>
<% end %>
ブラウザでテストすると、期待通りに動作します!コンソールにエラーが表示されなくなりました!
これは完全に有効な解決策ですが、意図しない副作用があるため、対処が必要かもしれません。この例のように、2番目の見積のフォームを開き、3番目の見積の「削除」ボタンをクリックすることを想像してください。
https://scrapbox.io/files/63001f607df4d4001dc38e82.png
さっそくブラウザでテストしてみましょう。予想通り3番目の見積が削除されましたが、このレスポンスでは2番目の見積のフォームも閉じられています。これは、3番目の引用を削除するフォームが "_top" フレームを対象としているため、ページ全体が置き換えられてしまうからです!
削除された見積を含むTurbo Frameのみを削除し、ページの残りの部分は変更せずに、ページの状態を維持することができればいいのですが。しかし、TurboとRailsが再び私たちの背中を押してくれました。先ほど「削除」ボタンに対して行ったことを取り除いてみましょう。
code:erb
<%# app/views/quotes/_quote.html.erb %>
<%= turbo_frame_tag quote do %>
<div class="quote">
<%= link_to quote.name,
quote_path(quote),
data: { turbo_frame: "_top" } %>
<div class="quote__actions">
<%= button_to "削除",
quote_path(quote),
method: :delete,
class: "btn btn--light" %>
<%= link_to "編集",
edit_quote_path(quote),
class: "btn btn--light" %>
</div>
</div>
<% end %>
いまがTURBO_STREAM形式について紹介するときです。
Turbo Stream形式
Rails 7のフォームは、TURBO_STREAMフォーマットで送信されるようになりました。見積もりを破棄して、Railsサーバーのログで何が起こるかを検証してみましょう。
code:txt
Started DELETE "/quotes/908005781" for 127.0.0.1 at 2022-01-27 15:30:13 +0100
Processing by QuotesController#destroy as TURBO_STREAM
見ての通り、QuotesControllerはTURBO_STREAMフォーマットで#destroyアクションを処理します。このフォーマットを使って何ができるのか、destroy アクションで削除された見積を含むTurbo Frameのみを削除し、ページの残りの部分はそのまま残すようにしてみましょう。
コントローラでは、responder_toメソッドのおかげで、以下のようにするとHTMLとTURBO_STREAMの両方のフォーマットに対応できます。
code:ruby
# app/controllers/quotes_controller.rb
def destroy
@quote.destroy
respond_to do |format|
format.html { redirect_to quotes_path, notice: "見積は正常に削除されました。" }
format.turbo_stream
end
end
他のフォーマットと同様に、対応するビューを作成しましょう。
code:erb
<%# app/views/quotes/destroy.turbo_stream.erb %>
<%= turbo_stream.remove "quote_#{@quote.id}" %>
見積を削除して、ブラウザの「ネットワーク」タブでレスポンス・ボディを検査してみましょう。ブラウザが受け取るHTMLは以下のようになるはずです。ただし、おそらく見積には別のIDが設定されているはずです。
code:html
<turbo-stream action="remove" target="quote_908005780">
</turbo-stream>
このHTMLはどこから来ているのでしょうか?先ほど作成したTURBO_STREAMビューで、turbo_streamヘルパーはremoveメソッドに"quote_#{@quote.id}"を引数として受け取っています。見ての通り、このヘルパーはこれをアクションremove、ターゲット "quote_908005780" を持つ<turbo-stream>カスタム要素に変換しています。
ブラウザがこのHTMLを受信すると、Turboはそれをどのように解釈すればよいかがわかります。Turboは、target属性で指定されたidを持つTurbo Frameに対して所望のアクションを実行します。この場合、Turboは削除された引用に対応するTurbo Frameを削除し、ページの残りの部分はそのままにします。これはまさに私たちが望んでいたことです。
----------
注)本章執筆時点では、turbo_streamヘルパーは以下のメソッドに応答するため、以下の動作が可能です。
(略)
----------
Turbo Framesと新しいTURBO_STREAMフォーマットの組み合わせにより、JavaScriptを1行も書かずにWebページの断片に対して正確な操作を行うことができ、Webページの状態を保存することができます。
次のセクションに移る前にもうひとつ、turbo_streamヘルパーはdom_idでも使用できます。このようにビューをリファクタリングすることができます。
code:erb
<%# app/views/quotes/destroy.turbo_stream.erb %>
<%= turbo_stream.remove @quote %>
いよいよ最後の機能である新規見積に取り組みます。
Turbo Framesで新規見積機能を作成する
最後に必要な機能は、見積書の作成です。実装に入る前に、これから作るもののスケッチを少し描いておきましょう。
「新規見積」ボタンをクリックしても、Quotes#new ページに移動することはなくなります。代わりに、新しい見積フォームが Quotes#index ページのヘッダーのすぐ下に表示されます。そして、「見積作成」ボタンをクリックすると、新しく作成された見積書がリストの先頭に追加され、新しい見積書フォームがページから削除されます。
これを行うには、さらに2つのTurbo Frameが必要です。
新しい見積フォームを受け取る空のTurbo Frameが1つ。
新規作成された見積を正しい位置に追加するために、見積の一覧をラップするTurbo Frameを1つ用意します。
スケッチで説明すると、以下のような感じです。
https://scrapbox.io/files/6300cd83674c9a001d586181.png
Quotes#indexページの空のTurboFrameと同じidのTurbo Frameで、Quotes#newページのフォームを囲みます。
https://scrapbox.io/files/6300ce8a19c5a7001db2bd6f.png
Turboフレームに適切な名前を付けて、「新規見積」ボタンをクリックすると、空のフレームである "new_quote" フレームの中身が、新しい見積を作成するためのフォームに置き換わります。
https://scrapbox.io/files/6300e18b674c9a001d58e7be.png
フォームを送信する際に、Turbo Streamsの力を借りて、新しい見積を見積の一覧の先頭に追加し、"new_quote" フレームを再び空になるように更新します。
https://scrapbox.io/files/6300e21da8b770001d01e5eb.png
では、この解決策を実装してみましょう。
前述したように、Quotes#indexビューとQuotes#newページにそれぞれ1つずつ、同じidのフレームが必要です。これらのフレームは dom_id(Quote.new) という ID を持ち、これは "new_quote" という文字列と同じです。
Quotes#newページでは、新規見積フォームをTurbo Frameで囲みましょう。
code:erb
<%# app/views/quotes/new.html.erb %>
<main class="container">
<%= link_to sanitize("&larr; 見積の一覧に戻る"), quotes_path %>
<div class="header">
<h1>新規見積</h1>
</div>
<%= turbo_frame_tag @quote do %>
<%= render "form", quote: @quote %>
<% end %>
</main>
ここで@quoteは新しいレコードなので、以下の3つの式は等価です。
code:ruby
turbo_frame_tag "new_quote"
turbo_frame_tag Quote.new
turbo_frame_tag @quote
では、この新規見積フォームを受け取る Quotes#index ページに、同じ ID の空の Turbo フレームを追加してみましょう。
code:erb
<%# app/views/quotes/index.html.erb %>
<main class="container">
<div class="header">
<h1>Quotes</h1>
<%= link_to "新規見積",
new_quote_path,
class: "btn btn--primary",
data: { turbo_frame: dom_id(Quote.new) } %>
</div>
<%= turbo_frame_tag Quote.new %>
<%= render @quotes %>
</main>
ご覧の通り、Quotes#indexのページではフレームが空になっています。新規見積フォームは、「新規見積」ボタンをクリックしたときだけ表示されるはずです。「新規見積」ボタンを先ほど作成した正しいTurbo Frameとリンクさせるために、data-turbo-frame data属性を使用する必要があります。
見ての通り、リンクの data-turbo-frame 属性が空の Turbo Frame の id と一致するため、ここで両者が接続されます。ここで何が起こるか分解してみましょう。
「新規見積」リンクをクリックすると、そのクリックはTurboによって傍受されます。
Turboは、「新規見積」リンクの data-turbo-frame属性のおかげで、id が new_quoteのフレームと対話する必要があることを認識します。
リクエストはAJAXで送信され、サーバーはidがnew_quoteであるフレームをもつQuotes#newページをレンダリングします。
ブラウザがHTMLを受信すると、TurboはQuotes#newページからnew_quoteというidのフレームを抽出し、Quotes#indexページの同じidの空のフレームを置き換えるのです!
それでは、ブラウザでテストしてみましょう。うまくいきました。予想通り、フォームがページに表示されました
それでは、「見積書作成」ボタンをクリックして、名前を空白にしてフォームを送信してみましょう。見積書は無効となり、ページ上にエラーが表示されるはずです。ここで何が起こるか説明しましょう。
「新規見積」ボタンをクリックすると、フォームの送信はTurboに横取りされます。
フォームがnew_quoteというIDのフレームでラップされているため、Turboはこのフレームを置き換えるだけでよいことがわかります。
サーバーはQuotesController#createアクションで無効なパラメータを受信し、エラーを含むフォームでQuotes#newビューをレンダリングします。
ブラウザがstatus: :unprocessable_entityのレスポンスを受け取ると、id new_quoteのフレームをエラーを含む新しいフレームに置き換えます。
あと少しです。スケッチで設計した機能を完成させるには、見積に有効な名前が与えられたときに、新しく作成した見積を見積一覧の先頭に挿入する必要があります。
今ブラウザでテストしてみると、見積はデータベースで作成されますが、作成された見積は見積一覧に追加されないことがわかります。
なぜでしょう?
有効な属性を持つフォームを送信すると、QuotesController#createアクションはQuotes#indexページをレンダリングし、そこには我々のフォームを置き換えるためのidがnew_quoteの空のフレームが含まれます。しかし、Turboは新しく作成された見積をどうすればいいのかわかりません。ページのどこに挿入すればいいのでしょうか?見積の一覧の末尾に追加するのでしょうか?それとも先頭に追加する?そこで、Turbo Streamビューを使用します。
QuotesController に、HTML と TURBO_STREAM の両方のフォーマットをサポートする必要があることを伝えましょう。
code:ruby
# app/controllers/quotes_controller.rb
def create
@quote = Quote.new(quote_params)
if @quote.save
respond_to do |format|
format.html { redirect_to quotes_path, notice: "見積は正常に作成されました。" }
format.turbo_stream
end
else
render :new, status: :unprocessable_entity
end
end
つづいて、対応するビューを作成しましょう。
code:erb
<%# app/views/quotes/create.turbo_stream.erb %>
<%= turbo_stream.prepend "quotes", partial: "quotes/quote", locals: { quote: @quote } %>
<%= turbo_stream.update Quote.new, "" %>
このビューでは、Turboに2つのことを行うように指示しています。
1行目は、id quotesを持つTurbo Frameに app/views/quotes/_quote.html.erb パーシャルを先頭に追加するようTurboに指示しています
2行目は、Turboに対して、id new_quoteのTurbo Frameを空のコンテンツで更新するように指示しています。
最後に、Quotes#indexページの見積一覧をラップするために、idが "quotes"のTurbo Frameを追加することで、動作させることができます。
code:erb
<%# app/views/quotes/index.html.erb %>
<div class="container">
<div class="header">
<h1>見積の一覧</h1>
<%= link_to "新規見積",
new_quote_path,
class: "btn btn--primary",
data: { turbo_frame: dom_id(Quote.new) } %>
</div>
<%= turbo_frame_tag Quote.new %>
<%= turbo_frame_tag "quotes" do %>
<%= render @quotes %>
<% end %>
</div>
では、ブラウザでテストしてみましょう。うまくいきました。有効な内容の新規見積フォームを送信する際に、開発ツールの「ネットワーク」タブを見ると、レスポンスボディには次のように表示されるはずです。
code:html
<turbo-stream action="prepend" target="quotes">
<template>
<turbo-frame id="quote_123">
<!-- The HTML for the quote partial -->
<turbo-frame>
</template>
</turbo-stream>
<turbo-stream action="update" target="new_quote">
<template>
<!-- An empty template! -->
</template>
</turbo-stream>
ご覧の通り、先ほど作成したcreate.turbo_stream.erbビューの2行がTurboが理解できる言語に翻訳されているのと一致します。レスポンスを受け取ると、TurboはtargetのTurbo Frame上でaction(append, prepend, replace, remove)を実行します。
注: Turbo Streamのビューには、同じことを書くにもさまざまな方法があります。先ほど作成したcreate.turbo_stream.erbのビューを見てみましょう。
(略)
見積の並べ替え
最後にもう一つ、注意しなければならないことがあります。見積の一覧の先頭に、作成された見積を追加することにしましたが、ページをリロードすると、リスト内の見積の順序が変わってしまいます。そこで、常に新しい順に見積を並べるために、Quoteモデルにスコープを追加してみましょう。
code:ruby
# app/models/quote.rb
class Quote < ApplicationRecord
validates :name, presence: true
scope :ordered, -> { order(id: :desc) }
end
そして、このスコープをコントローラ内の #index アクションで使用することにしましょう。
code:ruby
# app/controllers/quotes_controller.rb
def index
@quotes = Quote.ordered
end
これで、ページを更新しても、見積の順序は一定になりました。これは小さなことですが、何が起こっているのかが理解できることはユーザーにとって重要かもしれません。
この変更をおこなうと、見積の一覧の順序が同じでなくなるため、システムテストが壊れます。システムテストを更新して、再び合格するようにしましょう。
code:rb
# test/system/quotes_test.rb
setup do
# We need to order quote as well in the system tests
@quote = Quote.ordered.first
end
テストを実行してみましょう。すべて緑色になるはずです。見積もりエディタは、最初のスケッチで説明したとおりの外観になりました。新しいスキルを身につける必要がありましたが、実装は数行のコードで済みました。Turboは素晴らしいソフトウェアです。
キャンセルボタンの追加
さて、すべてが期待通りに動作するようになったところで、このページに最後の改良を加えてみましょう。新規作成/編集 した見積もりフォームを送信せずに閉じることができるようにしたいのです。そのために、quotes/_form.html.erb パーシャルに Quotes#index ページにリンクする "Cancel" リンクを追加することにします。
code:erb
<%# app/views/quotes/_form.html.erb %>
<%= simple_form_for quote, html: { class: "quote form" } do |f| %>
<% if quote.errors.any? %>
<div class="error-message">
<%= quote.errors.full_messages.to_sentence.capitalize %>
</div>
<% end %>
<%= f.input :name, input_html: { autofocus: true } %>
<%= link_to "Cancel", quotes_path, class: "btn btn--light" %>
<%= f.submit class: "btn btn--secondary" %>
<% end %>
ユーザーが新規見積フォームの「キャンセル」リンクをクリックしたとき、
このリンクはidがnew_quoteのTurbo Frame内にあるため、Turboはこのフレームのコンテンツのみを置き換えます
このリンクは Quotes#index ページへの移動です。このページはidがnew_quoteを持つ空のTurbo Frameを含みます
Turboはnew_quoteフレームのコンテンツを空のコンテンツに置き換えるため、フォームが消滅します
ユーザーが見積編集フォームの「キャンセル」リンクをクリックしたとき、
リンクはidがdom_id(quote)のTurbo Frame内にあるため、Turboはこのフレームのコンテンツのみを置き換えます。
このリンクはQuotes#indexページへの移動です。このページはdom_id(quote)のTurbo Frameを含みます。このTurbo Frameはキャンセルリンクをクリックした見積のHTMLを含みます
Turboはフォームを含むdom_id(quote) Frameのコンテンツをこの見積のHTMLに置き換えます
見積の作成と複数の見積の編集を同時に行おうとすると、ページの状態が保持されることに気がつきます。例えば、 見積を作成するとき、開かれたすべてのフォームが開かれたままになります。Turbo Frameはウェブページの独立した部分であり、カスタムのJavaScriptを書くことなく操作することができます。
まとめ
この章では、見積もりリソースに対する古典的なCRUDコントローラを、ほとんどコードもJavaScriptも使わない、最新のリアクティブ・アプリケーションに置き換えました。
では、作ったものを使って遊んでみましょう。見積を編集するフォームを開き、別の見積を破棄して、「新しい見積」ボタンをクリックしてみましょう。Reactのようなフロントエンドライブラリを使用する場合と比較して、管理すべき状態、ディスパッチすべき複雑なアクション、リデューサがありません...。Turboは楽しく作業できるのです
この章は内容が濃かったので、次の章に入る前に一息ついて、すべてが頭の中で整理されたことを確認しましょう
次の章では、Turbo StreamsをAction Cableでブロードキャストして、アプリケーションをリアルタイムに更新する方法について説明します。それでは、またお会いしましょう。
←前:
次: 5章 Trubo Streamによるリアルタイム更新 | Turbo Rails チュートリアル →